From: Aaron Schulz Date: Wed, 14 Sep 2016 09:21:18 +0000 (-0700) Subject: Move DBConnRef and ChronologyProtector to /libs/rdbms X-Git-Tag: 1.31.0-rc.0~5569^2 X-Git-Url: http://git.cyclocoop.org/%7D%7Cconcat%7B?a=commitdiff_plain;h=4f509e92ab3bcb5d98a540f4d15d173fd16baee3;p=lhc%2Fweb%2Fwiklou.git Move DBConnRef and ChronologyProtector to /libs/rdbms Change-Id: If2c4b314a5c39311328843f534d91bf90823e179 --- diff --git a/autoload.php b/autoload.php index 66736b3d42..557df832cf 100644 --- a/autoload.php +++ b/autoload.php @@ -241,7 +241,7 @@ $wgAutoloadLocalClasses = [ 'CheckStorage' => __DIR__ . '/maintenance/storage/checkStorage.php', 'CheckSyntax' => __DIR__ . '/maintenance/checkSyntax.php', 'CheckUsernames' => __DIR__ . '/maintenance/checkUsernames.php', - 'ChronologyProtector' => __DIR__ . '/includes/db/ChronologyProtector.php', + 'ChronologyProtector' => __DIR__ . '/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php', 'ClassCollector' => __DIR__ . '/includes/utils/AutoloadGenerator.php', 'CleanupAncientTables' => __DIR__ . '/maintenance/cleanupAncientTables.php', 'CleanupBlocks' => __DIR__ . '/maintenance/cleanupBlocks.php', @@ -299,7 +299,7 @@ $wgAutoloadLocalClasses = [ 'DBAccessBase' => __DIR__ . '/includes/dao/DBAccessBase.php', 'DBAccessError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBAccessObjectUtils' => __DIR__ . '/includes/dao/DBAccessObjectUtils.php', - 'DBConnRef' => __DIR__ . '/includes/db/DBConnRef.php', + 'DBConnRef' => __DIR__ . '/includes/libs/rdbms/database/RBConnRef.php', 'DBConnectionError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', 'DBExpectedError' => __DIR__ . '/includes/libs/rdbms/exception/DBError.php', diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php deleted file mode 100644 index 09b820ba2b..0000000000 --- a/includes/db/ChronologyProtector.php +++ /dev/null @@ -1,326 +0,0 @@ - position) */ - protected $startupPositions = []; - /** @var DBMasterPos[] Map of (DB master name => position) */ - protected $shutdownPositions = []; - /** @var float[] Map of (DB master name => 1) */ - protected $shutdownTouchDBs = []; - - /** @var integer Seconds to store positions */ - const POSITION_TTL = 60; - /** @var integer Max time to wait for positions to appear */ - const POS_WAIT_TIMEOUT = 5; - - /** - * @param BagOStuff $store - * @param array $client Map of (ip: , agent: ) - * @param float $posTime UNIX timestamp - * @since 1.27 - */ - public function __construct( BagOStuff $store, array $client, $posTime = null ) { - $this->store = $store; - $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] ); - $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId ); - $this->waitForPosTime = $posTime; - $this->logger = new \Psr\Log\NullLogger(); - } - - public function setLogger( LoggerInterface $logger ) { - $this->logger = $logger; - } - - /** - * @param bool $enabled Whether to no-op all method calls - * @since 1.27 - */ - public function setEnabled( $enabled ) { - $this->enabled = $enabled; - } - - /** - * @param bool $enabled Whether to check and wait on positions - * @since 1.27 - */ - public function setWaitEnabled( $enabled ) { - $this->wait = $enabled; - } - - /** - * Initialise a LoadBalancer to give it appropriate chronology protection. - * - * If the stash has a previous master position recorded, this will try to - * make sure that the next query to a replica DB of that master will see changes up - * to that position by delaying execution. The delay may timeout and allow stale - * data if no non-lagged replica DBs are available. - * - * @param LoadBalancer $lb - * @return void - */ - public function initLB( LoadBalancer $lb ) { - if ( !$this->enabled || $lb->getServerCount() <= 1 ) { - return; // non-replicated setup or disabled - } - - $this->initPositions(); - - $masterName = $lb->getServerName( $lb->getWriterIndex() ); - if ( !empty( $this->startupPositions[$masterName] ) ) { - $pos = $this->startupPositions[$masterName]; - $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" ); - $lb->waitFor( $pos ); - } - } - - /** - * Notify the ChronologyProtector that the LoadBalancer is about to shut - * down. Saves replication positions. - * - * @param LoadBalancer $lb - * @return void - */ - public function shutdownLB( LoadBalancer $lb ) { - if ( !$this->enabled ) { - return; // not enabled - } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) { - // Only save the position if writes have been done on the connection - return; - } - - $masterName = $lb->getServerName( $lb->getWriterIndex() ); - if ( $lb->getServerCount() > 1 ) { - $pos = $lb->getMasterPos(); - $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" ); - $this->shutdownPositions[$masterName] = $pos; - } else { - $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" ); - } - $this->shutdownTouchDBs[$masterName] = 1; - } - - /** - * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now. - * May commit chronology data to persistent storage. - * - * @param callable|null $workCallback Work to do instead of waiting on syncing positions - * @param string $mode One of (sync, async); whether to wait on remote datacenters - * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure - */ - public function shutdown( callable $workCallback = null, $mode = 'sync' ) { - if ( !$this->enabled ) { - return []; - } - - $store = $this->store; - // Some callers might want to know if a user recently touched a DB. - // These writes do not need to block on all datacenters receiving them. - foreach ( $this->shutdownTouchDBs as $dbName => $unused ) { - $store->set( - $this->getTouchedKey( $this->store, $dbName ), - microtime( true ), - $store::TTL_DAY - ); - } - - if ( !count( $this->shutdownPositions ) ) { - return []; // nothing to save - } - - $this->logger->info( __METHOD__ . ": saving master pos for " . - implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n" - ); - - // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local - // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This - // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT. - if ( $store->lock( $this->key, 3 ) ) { - if ( $workCallback ) { - // Let the store run the work before blocking on a replication sync barrier. By the - // time it's done with the work, the barrier should be fast if replication caught up. - $store->addBusyCallback( $workCallback ); - } - $ok = $store->set( - $this->key, - self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ), - self::POSITION_TTL, - ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0 - ); - $store->unlock( $this->key ); - } else { - $ok = false; - } - - if ( !$ok ) { - $bouncedPositions = $this->shutdownPositions; - // Raced out too many times or stash is down - $this->logger->warning( __METHOD__ . ": failed to save master pos for " . - implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n" - ); - } elseif ( $mode === 'sync' && - $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE - ) { - // Positions may not be in all datacenters, force LBFactory to play it safe - $this->logger->info( __METHOD__ . ": store may not support synchronous writes." ); - $bouncedPositions = $this->shutdownPositions; - } else { - $bouncedPositions = []; - } - - return $bouncedPositions; - } - - /** - * @param string $dbName DB master name (e.g. "db1052") - * @return float|bool UNIX timestamp when client last touched the DB; false if not on record - * @since 1.28 - */ - public function getTouched( $dbName ) { - return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) ); - } - - /** - * @param BagOStuff $store - * @param string $dbName - * @return string - */ - private function getTouchedKey( BagOStuff $store, $dbName ) { - return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName ); - } - - /** - * Load in previous master positions for the client - */ - protected function initPositions() { - if ( $this->initialized ) { - return; - } - - $this->initialized = true; - if ( $this->wait ) { - // If there is an expectation to see master positions with a certain min - // timestamp, then block until they appear, or until a timeout is reached. - if ( $this->waitForPosTime > 0.0 ) { - $data = null; - $loop = new WaitConditionLoop( - function () use ( &$data ) { - $data = $this->store->get( $this->key ); - - return ( self::minPosTime( $data ) >= $this->waitForPosTime ) - ? WaitConditionLoop::CONDITION_REACHED - : WaitConditionLoop::CONDITION_CONTINUE; - }, - $this->waitForPosTimeout - ); - $result = $loop->invoke(); - $waitedMs = $loop->getLastWaitTime() * 1e3; - - if ( $result == $loop::CONDITION_REACHED ) { - $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)"; - $this->logger->debug( $msg ); - } else { - $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)"; - $this->logger->info( $msg ); - } - } else { - $data = $this->store->get( $this->key ); - } - - $this->startupPositions = $data ? $data['positions'] : []; - $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" ); - } else { - $this->startupPositions = []; - $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" ); - } - } - - /** - * @param array|bool $data - * @return float|null - */ - private static function minPosTime( $data ) { - if ( !isset( $data['positions'] ) ) { - return null; - } - - $min = null; - foreach ( $data['positions'] as $pos ) { - /** @var DBMasterPos $pos */ - $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime(); - } - - return $min; - } - - /** - * @param array|bool $curValue - * @param DBMasterPos[] $shutdownPositions - * @return array - */ - private static function mergePositions( $curValue, array $shutdownPositions ) { - /** @var $curPositions DBMasterPos[] */ - if ( $curValue === false ) { - $curPositions = $shutdownPositions; - } else { - $curPositions = $curValue['positions']; - // Use the newest positions for each DB master - foreach ( $shutdownPositions as $db => $pos ) { - if ( !isset( $curPositions[$db] ) - || $pos->asOfTime() > $curPositions[$db]->asOfTime() - ) { - $curPositions[$db] = $pos; - } - } - } - - return [ 'positions' => $curPositions ]; - } -} diff --git a/includes/db/DBConnRef.php b/includes/db/DBConnRef.php deleted file mode 100644 index 8604295385..0000000000 --- a/includes/db/DBConnRef.php +++ /dev/null @@ -1,581 +0,0 @@ -lb = $lb; - if ( $conn instanceof DatabaseBase ) { - $this->conn = $conn; - } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) { - $this->params = $conn; - } else { - throw new InvalidArgumentException( "Missing lazy connection arguments." ); - } - } - - function __call( $name, array $arguments ) { - if ( $this->conn === null ) { - list( $db, $groups, $wiki ) = $this->params; - $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); - } - - return call_user_func_array( [ $this->conn, $name ], $arguments ); - } - - public function getServerInfo() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function bufferResults( $buffer = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function trxLevel() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function trxTimestamp() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function explicitTrxActive() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function tablePrefix( $prefix = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function dbSchema( $schema = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getLBInfo( $name = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setLBInfo( $name, $value = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function implicitGroupby() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function implicitOrderby() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lastQuery() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function doneWrites() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lastDoneWrites() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function writesPending() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function writesOrCallbacksPending() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function pendingWriteCallers() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function isOpen() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function restoreFlags( $state = self::RESTORE_PRIOR ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getFlag( $flag ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getProperty( $name ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getWikiID() { - if ( $this->conn === null ) { - // Avoid triggering a connection - return $this->params[self::FLD_WIKI]; - } - - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getType() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function open( $server, $user, $password, $dbName ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function fetchObject( $res ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function fetchRow( $res ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function numRows( $res ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function numFields( $res ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function fieldName( $res, $n ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function insertId() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function dataSeek( $res, $row ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lastErrno() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lastError() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function fieldInfo( $table, $field ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function affectedRows() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getSoftwareLink() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getServerVersion() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function close() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function reportConnectionError( $error = 'Unknown error' ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function freeResult( $res ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectField( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectFieldValues( - $table, $var, $cond = '', $fname = __METHOD__, $options = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function select( - $table, $vars, $conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectSQLText( - $table, $vars, $conds = '', $fname = __METHOD__, - $options = [], $join_conds = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectRow( - $table, $vars, $conds, $fname = __METHOD__, - $options = [], $join_conds = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function estimateRowCount( - $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectRowCount( - $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function fieldExists( $table, $field, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function indexExists( $table, $index, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function tableExists( $table, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function indexUnique( $table, $index ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function insert( $table, $a, $fname = __METHOD__, $options = [] ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function makeList( $a, $mode = LIST_COMMA ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function makeWhereFrom2d( $data, $baseKey, $subKey ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function bitNot( $field ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function bitAnd( $fieldLeft, $fieldRight ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function bitOr( $fieldLeft, $fieldRight ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function buildConcat( $stringList ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function buildGroupConcatField( - $delim, $table, $field, $conds = '', $join_conds = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function selectDB( $db ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getDBname() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getServer() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function addQuotes( $s ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function buildLike() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function anyChar() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function anyString() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function nextSequenceValue( $seqName ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function upsert( - $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function deleteJoin( - $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function delete( $table, $conds, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function insertSelect( - $destTable, $srcTable, $varMap, $conds, - $fname = __METHOD__, $insertOptions = [], $selectOptions = [] - ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function unionSupportsOrderAndLimit() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function unionQueries( $sqls, $all ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function conditional( $cond, $trueVal, $falseVal ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function strreplace( $orig, $old, $new ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getServerUptime() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function wasDeadlock() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function wasLockTimeout() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function wasErrorReissuable() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function wasReadOnlyError() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function masterPosWait( DBMasterPos $pos, $timeout ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getSlavePos() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getMasterPos() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function serverIsReadOnly() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function onTransactionResolution( callable $callback ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function onTransactionIdle( callable $callback ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function onTransactionPreCommitOrIdle( callable $callback ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setTransactionListener( $name, callable $callback = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function startAtomic( $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function endAtomic( $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function doAtomicSection( $fname, callable $callback ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function commit( $fname = __METHOD__, $flush = '' ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function rollback( $fname = __METHOD__, $flush = '' ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function flushSnapshot( $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function listTables( $prefix = null, $fname = __METHOD__ ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function timestamp( $ts = 0 ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function timestampOrNull( $ts = null ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function ping( &$rtt = null ) { - return func_num_args() - ? $this->__call( __FUNCTION__, [ &$rtt ] ) - : $this->__call( __FUNCTION__, [] ); // method cares about null vs missing - } - - public function getLag() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getSessionLagStatus() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function maxListLen() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function encodeBlob( $b ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function decodeBlob( $b ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setSessionOptions( array $options ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setSchemaVars( $vars ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lockIsFree( $lockName, $method ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function lock( $lockName, $method, $timeout = 5 ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function unlock( $lockName, $method ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function namedLocksEnqueue() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function getInfinity() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function encodeExpiry( $expiry ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function decodeExpiry( $expiry, $format = TS_MW ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function setBigSelects( $value = true ) { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - public function isReadOnly() { - return $this->__call( __FUNCTION__, func_get_args() ); - } - - /** - * Clean up the connection when out of scope - */ - function __destruct() { - if ( $this->conn !== null ) { - $this->lb->reuseConnection( $this->conn ); - } - } -} diff --git a/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php new file mode 100644 index 0000000000..09b820ba2b --- /dev/null +++ b/includes/libs/rdbms/chronologyprotector/ChronologyProtector.php @@ -0,0 +1,326 @@ + position) */ + protected $startupPositions = []; + /** @var DBMasterPos[] Map of (DB master name => position) */ + protected $shutdownPositions = []; + /** @var float[] Map of (DB master name => 1) */ + protected $shutdownTouchDBs = []; + + /** @var integer Seconds to store positions */ + const POSITION_TTL = 60; + /** @var integer Max time to wait for positions to appear */ + const POS_WAIT_TIMEOUT = 5; + + /** + * @param BagOStuff $store + * @param array $client Map of (ip: , agent: ) + * @param float $posTime UNIX timestamp + * @since 1.27 + */ + public function __construct( BagOStuff $store, array $client, $posTime = null ) { + $this->store = $store; + $this->clientId = md5( $client['ip'] . "\n" . $client['agent'] ); + $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId ); + $this->waitForPosTime = $posTime; + $this->logger = new \Psr\Log\NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @param bool $enabled Whether to no-op all method calls + * @since 1.27 + */ + public function setEnabled( $enabled ) { + $this->enabled = $enabled; + } + + /** + * @param bool $enabled Whether to check and wait on positions + * @since 1.27 + */ + public function setWaitEnabled( $enabled ) { + $this->wait = $enabled; + } + + /** + * Initialise a LoadBalancer to give it appropriate chronology protection. + * + * If the stash has a previous master position recorded, this will try to + * make sure that the next query to a replica DB of that master will see changes up + * to that position by delaying execution. The delay may timeout and allow stale + * data if no non-lagged replica DBs are available. + * + * @param LoadBalancer $lb + * @return void + */ + public function initLB( LoadBalancer $lb ) { + if ( !$this->enabled || $lb->getServerCount() <= 1 ) { + return; // non-replicated setup or disabled + } + + $this->initPositions(); + + $masterName = $lb->getServerName( $lb->getWriterIndex() ); + if ( !empty( $this->startupPositions[$masterName] ) ) { + $pos = $this->startupPositions[$masterName]; + $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" ); + $lb->waitFor( $pos ); + } + } + + /** + * Notify the ChronologyProtector that the LoadBalancer is about to shut + * down. Saves replication positions. + * + * @param LoadBalancer $lb + * @return void + */ + public function shutdownLB( LoadBalancer $lb ) { + if ( !$this->enabled ) { + return; // not enabled + } elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) { + // Only save the position if writes have been done on the connection + return; + } + + $masterName = $lb->getServerName( $lb->getWriterIndex() ); + if ( $lb->getServerCount() > 1 ) { + $pos = $lb->getMasterPos(); + $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" ); + $this->shutdownPositions[$masterName] = $pos; + } else { + $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" ); + } + $this->shutdownTouchDBs[$masterName] = 1; + } + + /** + * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now. + * May commit chronology data to persistent storage. + * + * @param callable|null $workCallback Work to do instead of waiting on syncing positions + * @param string $mode One of (sync, async); whether to wait on remote datacenters + * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure + */ + public function shutdown( callable $workCallback = null, $mode = 'sync' ) { + if ( !$this->enabled ) { + return []; + } + + $store = $this->store; + // Some callers might want to know if a user recently touched a DB. + // These writes do not need to block on all datacenters receiving them. + foreach ( $this->shutdownTouchDBs as $dbName => $unused ) { + $store->set( + $this->getTouchedKey( $this->store, $dbName ), + microtime( true ), + $store::TTL_DAY + ); + } + + if ( !count( $this->shutdownPositions ) ) { + return []; // nothing to save + } + + $this->logger->info( __METHOD__ . ": saving master pos for " . + implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n" + ); + + // CP-protected writes should overwhemingly go to the master datacenter, so get DC-local + // lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This + // makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT. + if ( $store->lock( $this->key, 3 ) ) { + if ( $workCallback ) { + // Let the store run the work before blocking on a replication sync barrier. By the + // time it's done with the work, the barrier should be fast if replication caught up. + $store->addBusyCallback( $workCallback ); + } + $ok = $store->set( + $this->key, + self::mergePositions( $store->get( $this->key ), $this->shutdownPositions ), + self::POSITION_TTL, + ( $mode === 'sync' ) ? $store::WRITE_SYNC : 0 + ); + $store->unlock( $this->key ); + } else { + $ok = false; + } + + if ( !$ok ) { + $bouncedPositions = $this->shutdownPositions; + // Raced out too many times or stash is down + $this->logger->warning( __METHOD__ . ": failed to save master pos for " . + implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n" + ); + } elseif ( $mode === 'sync' && + $store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE + ) { + // Positions may not be in all datacenters, force LBFactory to play it safe + $this->logger->info( __METHOD__ . ": store may not support synchronous writes." ); + $bouncedPositions = $this->shutdownPositions; + } else { + $bouncedPositions = []; + } + + return $bouncedPositions; + } + + /** + * @param string $dbName DB master name (e.g. "db1052") + * @return float|bool UNIX timestamp when client last touched the DB; false if not on record + * @since 1.28 + */ + public function getTouched( $dbName ) { + return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) ); + } + + /** + * @param BagOStuff $store + * @param string $dbName + * @return string + */ + private function getTouchedKey( BagOStuff $store, $dbName ) { + return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName ); + } + + /** + * Load in previous master positions for the client + */ + protected function initPositions() { + if ( $this->initialized ) { + return; + } + + $this->initialized = true; + if ( $this->wait ) { + // If there is an expectation to see master positions with a certain min + // timestamp, then block until they appear, or until a timeout is reached. + if ( $this->waitForPosTime > 0.0 ) { + $data = null; + $loop = new WaitConditionLoop( + function () use ( &$data ) { + $data = $this->store->get( $this->key ); + + return ( self::minPosTime( $data ) >= $this->waitForPosTime ) + ? WaitConditionLoop::CONDITION_REACHED + : WaitConditionLoop::CONDITION_CONTINUE; + }, + $this->waitForPosTimeout + ); + $result = $loop->invoke(); + $waitedMs = $loop->getLastWaitTime() * 1e3; + + if ( $result == $loop::CONDITION_REACHED ) { + $msg = "expected and found pos time {$this->waitForPosTime} ({$waitedMs}ms)"; + $this->logger->debug( $msg ); + } else { + $msg = "expected but missed pos time {$this->waitForPosTime} ({$waitedMs}ms)"; + $this->logger->info( $msg ); + } + } else { + $data = $this->store->get( $this->key ); + } + + $this->startupPositions = $data ? $data['positions'] : []; + $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" ); + } else { + $this->startupPositions = []; + $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" ); + } + } + + /** + * @param array|bool $data + * @return float|null + */ + private static function minPosTime( $data ) { + if ( !isset( $data['positions'] ) ) { + return null; + } + + $min = null; + foreach ( $data['positions'] as $pos ) { + /** @var DBMasterPos $pos */ + $min = $min ? min( $pos->asOfTime(), $min ) : $pos->asOfTime(); + } + + return $min; + } + + /** + * @param array|bool $curValue + * @param DBMasterPos[] $shutdownPositions + * @return array + */ + private static function mergePositions( $curValue, array $shutdownPositions ) { + /** @var $curPositions DBMasterPos[] */ + if ( $curValue === false ) { + $curPositions = $shutdownPositions; + } else { + $curPositions = $curValue['positions']; + // Use the newest positions for each DB master + foreach ( $shutdownPositions as $db => $pos ) { + if ( !isset( $curPositions[$db] ) + || $pos->asOfTime() > $curPositions[$db]->asOfTime() + ) { + $curPositions[$db] = $pos; + } + } + } + + return [ 'positions' => $curPositions ]; + } +} diff --git a/includes/libs/rdbms/database/RBConnRef.php b/includes/libs/rdbms/database/RBConnRef.php new file mode 100644 index 0000000000..e606340b91 --- /dev/null +++ b/includes/libs/rdbms/database/RBConnRef.php @@ -0,0 +1,581 @@ +lb = $lb; + if ( $conn instanceof IDatabase ) { + $this->conn = $conn; // live handle + } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) { + $this->params = $conn; + } else { + throw new InvalidArgumentException( "Missing lazy connection arguments." ); + } + } + + function __call( $name, array $arguments ) { + if ( $this->conn === null ) { + list( $db, $groups, $wiki ) = $this->params; + $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); + } + + return call_user_func_array( [ $this->conn, $name ], $arguments ); + } + + public function getServerInfo() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bufferResults( $buffer = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function trxLevel() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function trxTimestamp() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function explicitTrxActive() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function tablePrefix( $prefix = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function dbSchema( $schema = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getLBInfo( $name = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setLBInfo( $name, $value = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function implicitGroupby() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function implicitOrderby() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastQuery() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function doneWrites() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastDoneWrites() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function writesPending() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function writesOrCallbacksPending() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function pendingWriteCallers() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function isOpen() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function restoreFlags( $state = self::RESTORE_PRIOR ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getFlag( $flag ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getProperty( $name ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getWikiID() { + if ( $this->conn === null ) { + // Avoid triggering a connection + return $this->params[self::FLD_WIKI]; + } + + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getType() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function open( $server, $user, $password, $dbName ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fetchObject( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fetchRow( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function numRows( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function numFields( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldName( $res, $n ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insertId() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function dataSeek( $res, $row ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastErrno() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lastError() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldInfo( $table, $field ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function affectedRows() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getSoftwareLink() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServerVersion() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function close() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function reportConnectionError( $error = 'Unknown error' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function freeResult( $res ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectField( + $table, $var, $cond = '', $fname = __METHOD__, $options = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectFieldValues( + $table, $var, $cond = '', $fname = __METHOD__, $options = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function select( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectSQLText( + $table, $vars, $conds = '', $fname = __METHOD__, + $options = [], $join_conds = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectRow( + $table, $vars, $conds, $fname = __METHOD__, + $options = [], $join_conds = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function estimateRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectRowCount( + $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function fieldExists( $table, $field, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function indexExists( $table, $index, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function tableExists( $table, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function indexUnique( $table, $index ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insert( $table, $a, $fname = __METHOD__, $options = [] ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function makeList( $a, $mode = LIST_COMMA ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function makeWhereFrom2d( $data, $baseKey, $subKey ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitNot( $field ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitAnd( $fieldLeft, $fieldRight ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function bitOr( $fieldLeft, $fieldRight ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildConcat( $stringList ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function selectDB( $db ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getDBname() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServer() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function addQuotes( $s ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function buildLike() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function anyChar() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function anyString() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function nextSequenceValue( $seqName ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function upsert( + $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function deleteJoin( + $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function delete( $table, $conds, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function insertSelect( + $destTable, $srcTable, $varMap, $conds, + $fname = __METHOD__, $insertOptions = [], $selectOptions = [] + ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unionSupportsOrderAndLimit() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unionQueries( $sqls, $all ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function conditional( $cond, $trueVal, $falseVal ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function strreplace( $orig, $old, $new ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getServerUptime() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasDeadlock() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasLockTimeout() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasErrorReissuable() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function wasReadOnlyError() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function masterPosWait( DBMasterPos $pos, $timeout ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getSlavePos() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getMasterPos() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function serverIsReadOnly() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function onTransactionResolution( callable $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function onTransactionIdle( callable $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function onTransactionPreCommitOrIdle( callable $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setTransactionListener( $name, callable $callback = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function startAtomic( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function endAtomic( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function doAtomicSection( $fname, callable $callback ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function commit( $fname = __METHOD__, $flush = '' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function rollback( $fname = __METHOD__, $flush = '' ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function flushSnapshot( $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function listTables( $prefix = null, $fname = __METHOD__ ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function timestamp( $ts = 0 ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function timestampOrNull( $ts = null ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function ping( &$rtt = null ) { + return func_num_args() + ? $this->__call( __FUNCTION__, [ &$rtt ] ) + : $this->__call( __FUNCTION__, [] ); // method cares about null vs missing + } + + public function getLag() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getSessionLagStatus() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function maxListLen() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function encodeBlob( $b ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function decodeBlob( $b ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setSessionOptions( array $options ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setSchemaVars( $vars ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lockIsFree( $lockName, $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function lock( $lockName, $method, $timeout = 5 ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function unlock( $lockName, $method ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function namedLocksEnqueue() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function getInfinity() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function encodeExpiry( $expiry ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function decodeExpiry( $expiry, $format = TS_MW ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function setBigSelects( $value = true ) { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + public function isReadOnly() { + return $this->__call( __FUNCTION__, func_get_args() ); + } + + /** + * Clean up the connection when out of scope + */ + function __destruct() { + if ( $this->conn !== null ) { + $this->lb->reuseConnection( $this->conn ); + } + } +}